﻿using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

namespace InfiniTec.Security.Certificates
{
    public static class SelfSignedCertificate
    {
    	private const string OidSubjectAltName = "2.5.29.7";

		public static X509Certificate2 Create(CertificateInfo certificateInfo)
		{
			byte[] encodedName;
			uint cbName = 0;
			string keyContainer = Guid.NewGuid().ToString();

			if (SafeNativeMethods.CertStrToName(X509Encoding.AsnString, certificateInfo.DistinguishedName, X509Encoding.X500NameString | X509Encoding.ReverseRdns, IntPtr.Zero, null, ref cbName, IntPtr.Zero))
			{
				encodedName = new byte[cbName];
				SafeNativeMethods.CertStrToName(X509Encoding.AsnString, certificateInfo.DistinguishedName, X509Encoding.X500NameString | X509Encoding.ReverseRdns, IntPtr.Zero, encodedName, ref cbName, IntPtr.Zero);
			}
			else
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}


			var subjectblob = new CERT_NAME_BLOB {pbData = Marshal.AllocHGlobal(encodedName.Length)};
			Marshal.Copy(encodedName, 0, subjectblob.pbData, encodedName.Length);
			subjectblob.cbData = encodedName.Length;

			IntPtr context;

			bool contextAcquired = SafeNativeMethods.CryptAcquireContext(out context, keyContainer, certificateInfo.ProviderName,
			                                                             CryptographicProvider.RsaFull, KeyContainerOptions.CreateKeyset | KeyContainerOptions.UseMachineKeySet);
			if (!contextAcquired)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}

			IntPtr signKey;

			SysTime start = ConvertDateTimeToSysTime(certificateInfo.StartDate);
			SysTime end = ConvertDateTimeToSysTime(certificateInfo.EndDate);

			if (!SafeNativeMethods.CryptGenKey(context, KeySpecification.KeyExchange, 1, out signKey))
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}


			var pInfo = new CRYPT_KEY_PROV_INFO
			            	{
			            		pwszContainerName = keyContainer, pwszProvName = certificateInfo.ProviderName, dwProvType = certificateInfo.Provider,
			            		dwFlags = CspOptions.KeepHandleOpen, dwKeySpec = certificateInfo.KeySpecification
			            	};

			var extensions = new CertExtensions();
			IntPtr hCertCntxt = SafeNativeMethods.CertCreateSelfSignCertificate(
				context, ref subjectblob, 0, pInfo, IntPtr.Zero, start, end, ref extensions);

			IntPtr certificateCopy;

			if (!SafeNativeMethods.CertAddCertificateContextToStore(
					 certificateInfo.TargetStore.StoreHandle, hCertCntxt, 4 /*CERT_STORE_ADD_Always*/,
					 out certificateCopy))
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}

			SafeNativeMethods.CertFreeCertificateContext(hCertCntxt);

			pInfo = new CRYPT_KEY_PROV_INFO
			        	{
			        		pwszContainerName = keyContainer, pwszProvName = certificateInfo.ProviderName, dwProvType = CryptographicProvider.RsaFull,
			        		dwFlags = CspOptions.UseMachineKeyset, dwKeySpec = KeySpecification.KeyExchange
			        	};

			if (!SafeNativeMethods.CertSetCertificateContextProperty(certificateCopy, 2, 0, pInfo))
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}

			var result = new X509Certificate2(certificateCopy);

			SafeNativeMethods.CryptDestroyKey(signKey);

			if (hCertCntxt == IntPtr.Zero)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}


			Marshal.FreeHGlobal(subjectblob.pbData);


			SafeNativeMethods.CryptReleaseContext(context, 0);

			return result;

		}

		//private static CertExtensions CreateExtensions(CertificateInfo info)
		//{
		//    CERT_ALT_NAME_ENTRY[] items = info.AlternateSubjectNames.Select(name => new CERT_ALT_NAME_ENTRY
		//                                                                                {
		//                                                                                    dwAltNameChoice = name.NameType,
		//                                                                                    Name = Marshal.StringToHGlobalUni(name.Name)
		//                                                                                    /*Entry = new CERT_ALT_NAME
		//                                                                                                {
		//                                                                                                    pwszDNSName = name.Name        		
		//                                                                                                }*/
																							
		//                                                                                }).ToArray();

		//    IntPtr itemBlob = Marshal.AllocHGlobal(items.Length * Marshal.SizeOf(typeof (CERT_ALT_NAME_ENTRY)));

		//    for (int i = 0; i < items.Length; i++)
		//    {
		//        var offset = (IntPtr) ((long) itemBlob + i * Marshal.SizeOf(typeof (CERT_ALT_NAME_ENTRY)));
		//        Marshal.StructureToPtr(items[i], offset, false);
		//    }

		//    var altNameInfo = new CERT_ALT_NAME_INFO {cAltEntry = (uint) items.Length, rgAltEntry = itemBlob };

		//    IntPtr extensionBlob = IntPtr.Zero;
		//    int size = 0;
		//    var statusCode = SafeNativeMethods.CryptEncodeObjectEx(CertEncodingType.X509_ASN_ENCODING | CertEncodingType.PKCS_7_ASN_ENCODING, 
		//        new IntPtr(12), ref altNameInfo,
		//                                                   EncodeObjectFlags.CRYPT_ENCODE_ALLOC_FLAG, 
		//                                                   IntPtr.Zero, ref extensionBlob, ref size);

		//    Marshal.FreeHGlobal(itemBlob);

		//    if (!statusCode)
		//    {
		//        throw new Win32Exception(Marshal.GetLastWin32Error());
		//    }

		//    var extension = new CERT_EXTENSION
		//                        {
		//                            ExtensionOid = OidSubjectAltName,
		//                            IsCritical = true,
		//                            Value = new CRYPTOAPI_BLOB
		//                                        {
		//                                            Length= (uint) size,
		//                                            Data = extensionBlob
		//                                        }
		//                        };
		//    var result = new CertExtensions
		//                    {
		//                        cExtension = 1,
		//                        rgExtension = extension
		//                    };

		//    return result;
		//}

    	private static SysTime ConvertDateTimeToSysTime(DateTime? date)
        {
            if (date == null) return null;

            var result = new SysTime
                         	{
                         		wDay = ((ushort) date.Value.Day), wDayOfWeek = ((ushort) date.Value.DayOfWeek), wMonth = ((ushort) date.Value.Month),
                         		wYear = ((ushort) date.Value.Year), wHour = ((ushort) date.Value.Hour), wMinute = ((ushort) date.Value.Minute),
                         		wSecond = ((ushort) date.Value.Second), wMilliseconds = ((ushort) date.Value.Millisecond)
                         	};

        	return result;
        }
    }
}